@ragrabbit/mcp

by madarco
Verified
import React, { useMemo } from "react"; import { authOrLogin } from "@repo/auth"; import db from "@repo/db"; import { and, eq, not, count, sql } from "@repo/db/drizzle"; import { chatsTable, messagesTable } from "@repo/db/schema"; import { ArrowLeft, Calendar, Hash, MessageSquare, User, Type, ChevronDown, Bot, Terminal, } from "@repo/design/base/icons"; import { Button } from "@repo/design/shadcn/button"; import { Card, CardContent } from "@repo/design/shadcn/card"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@repo/design/shadcn/collapsible"; import { SourceBoxList } from "@repo/design/components/chat/source-box"; import Link from "next/link"; import { notFound } from "next/navigation"; function capitalize(str: string) { return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); } function formatTimeDelta(startDate: Date, currentDate: Date): string { const deltaMs = currentDate.getTime() - startDate.getTime(); const deltaSeconds = Math.floor(deltaMs / 1000); if (deltaSeconds < 60) { return `+${deltaSeconds}s`; } const deltaMinutes = Math.floor(deltaSeconds / 60); const remainingSeconds = deltaSeconds % 60; return `+${deltaMinutes}m ${remainingSeconds}s`; } export default async function ChatDetailPage({ params }: { params: { id: string } }) { const session = await authOrLogin(); const { id } = await params; // Fetch chat with message count and total tokens const chatStats = await db .select({ chat: chatsTable, messageCount: count(sql<number>`CASE WHEN ${messagesTable.role} != 'system' THEN 1 END`), totalTokens: sql<number>`sum(${messagesTable.tokenCount})`, }) .from(chatsTable) .where(and(eq(chatsTable.id, id), eq(chatsTable.organizationId, session.user.organizationId))) .leftJoin(messagesTable, eq(chatsTable.id, messagesTable.chatId)) .groupBy(chatsTable.id) .limit(1); const chat = chatStats[0]?.chat; if (!chat) { notFound(); } // Fetch messages separately const messages = await db .select() .from(messagesTable) .where(eq(messagesTable.chatId, id)) .orderBy(messagesTable.createdAt); return ( <> <div className="flex items-center justify-between space-y-2 mb-8"> <div className="space-y-1"> <div className="flex items-center gap-2"> <Button variant="outline" size="icon" asChild className="h-8 w-8"> <Link href="/dashboard/chats"> <ArrowLeft className="h-4 w-4" /> </Link> </Button> <h2 className="text-2xl font-bold tracking-tight"> {chat.title ? capitalize(chat.title) : "Untitled chat"} </h2> </div> <p className="text-muted-foreground">View chat details and messages</p> </div> </div> <div className="space-y-4"> <Card> <CardContent className="pt-6"> <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4"> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Calendar className="h-4 w-4" /> Created </div> <div className="font-medium">{chat.createdAt?.toLocaleString()}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <User className="h-4 w-4" /> User </div> <div className="font-medium">{chat.userId}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <MessageSquare className="h-4 w-4" /> Messages </div> <div className="font-medium">{chatStats[0]?.messageCount}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Type className="h-4 w-4" /> Total Tokens </div> <div className="font-medium">{chatStats[0]?.totalTokens || 0}</div> </div> <div className="flex flex-col gap-1"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Hash className="h-4 w-4" /> IP Address </div> <div className="font-medium">{chat.ip || "Unknown"}</div> </div> </div> </CardContent> </Card> <Card> <CardContent className="pt-6 space-y-6"> <div className="flex items-center justify-between"> <div className="space-y-1"> <h3 className="text-lg font-semibold">Messages</h3> <p className="text-sm text-muted-foreground">{messages.length} total messages</p> </div> </div> <div className="space-y-4"> {messages.map((message, i) => { const messageDate = message.createdAt; const firstMessageDate = messages[0]?.createdAt; const timeDisplay = i === 0 ? messageDate?.toLocaleString() : messageDate && firstMessageDate ? formatTimeDelta(firstMessageDate, messageDate) : ""; return ( <div key={i} className="border rounded-lg"> <div className="border-b bg-muted/50 px-4 py-2 flex items-center justify-between"> <div className="font-medium capitalize flex items-center gap-2"> {message.role === "user" && <User className="h-4 w-4" />} {message.role === "system" && <Terminal className="h-4 w-4" />} {message.role === "assistant" && <Bot className="h-4 w-4" />} {message.role} </div> <div className="flex items-center gap-4"> <div className="text-sm text-muted-foreground flex items-center gap-2"> <Type className="h-4 w-4" /> {message.tokenCount || 0} </div> <div className="text-sm text-muted-foreground">{timeDisplay}</div> </div> </div> {message.role === "system" ? ( <Collapsible> <CollapsibleTrigger asChild> <Button variant="ghost" className="w-full flex items-center gap-2 justify-start p-2 h-8"> <ChevronDown className="h-4 w-4" /> <span className="text-sm font-medium">Show system message</span> </Button> </CollapsibleTrigger> <CollapsibleContent> <div className="p-4 pt-0"> <pre className="whitespace-pre-wrap text-sm max-h-[200px] overflow-y-auto"> {message.content} </pre> </div> </CollapsibleContent> </Collapsible> ) : ( <div className="p-4"> <pre className="whitespace-pre-wrap text-sm max-h-[200px] overflow-y-auto"> {message.content} </pre> {message.reasoning && ( <> <div className="mt-4 pt-4 border-t"> <div className="text-sm font-medium mb-2">Reasoning:</div> <pre className="whitespace-pre-wrap text-sm text-muted-foreground"> {message.reasoning} </pre> </div> </> )} {message.annotations && Object.keys(message.annotations).length > 0 && ( <div className="mt-4 pt-4 border-t"> <Collapsible> <CollapsibleTrigger asChild> <Button variant="ghost" className="flex items-center gap-2 p-0 h-6"> <ChevronDown className="h-4 w-4" /> <span className="text-sm font-medium"> Annotations ({Object.keys(message.annotations).length}) </span> </Button> </CollapsibleTrigger> <CollapsibleContent className="mt-2"> <div className="space-y-2"> {Object.entries(message.annotations).map(([name, annotation], index) => { if (name === "sources") { return ( <div key={index}> <div className="font-medium mb-2">Sources</div> <SourceBoxList sources={annotation.data} /> </div> ); } if (name === "suggestedPrompts") { return ( <div key={index}> <div className="font-medium mb-2">Suggested Prompts </div> <div className="flex flex-wrap gap-2"> {annotation.data.map((query: string, i: number) => ( <Button key={i} variant="outline" className="py-1 md:py-2"> {query} </Button> ))} </div> </div> ); } return ( <div key={index} className="text-sm text-muted-foreground bg-muted/50 p-2 rounded" > <div className="font-medium mb-1">{name}</div> <pre className="whitespace-pre-wrap">{JSON.stringify(annotation, null, 2)}</pre> </div> ); })} </div> </CollapsibleContent> </Collapsible> </div> )} </div> )} </div> ); })} {messages.length === 0 && ( <div className="text-center py-4 text-muted-foreground">No messages available</div> )} </div> </CardContent> </Card> </div> </> ); }